﻿/////////////////////////////////////////////////////////////////////////////////////////////////////
// Original Code by : Michael Dela Cuesta (michael.dcuesta@gmail.com)                              //
// Source Code Available : http://anito.codeplex.com                                               //
//                                                                                                 // 
// This source code is made available under the terms of the Microsoft Public License (MS-PL)      // 
///////////////////////////////////////////////////////////////////////////////////////////////////// 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;

namespace Anito.Data.Schema
{
    [Serializable]
    [XmlRoot("DataObject",
        IsNullable = false)]
    public class TypeTable: List<TypeColumn>, IXmlSerializable
    {
        private bool m_isHasIdentitySet;
        private bool m_isHasKeySet;
        private bool m_hasIdentity;
        private bool m_hasKey;
        private string m_columnList;
        private List<TypeRelation> m_relations;
        private string m_assemblyName = string.Empty;
        private TypeColumn m_identityColumn;

        private string TypeName { get; set; }

        public Type MappedObjectType { get; private set; }

        public List<TypeRelation> Associations
        {
            get
            {
                m_relations = m_relations ?? new List<TypeRelation>();
                return m_relations;
            }
        }

        public string ViewSource { get; set; }
        public string UpdateSource { get; set; }

        public string ColumnList
        {
            get
            {
                if (m_columnList == null)
                {
                    var sb = new StringBuilder();
                    foreach (var column in this)
                    {
                        if (sb.Length > 0)
                            sb.Append(", ");
                        sb.Append(column.Name);
                    }
                    m_columnList = sb.ToString();
                }
                return m_columnList;
            }
        }

        public TypeTable()
        { 
        
        }

        public TypeTable(Type type)
        {
            ViewSource = type.Name;
            UpdateSource = type.Name;
            TypeName = type.FullName;
            m_assemblyName = type.Assembly.FullName;

            var tattr = type.GetCustomAttributes(true);
            for (int i = 0; i < tattr.Length; i++)
            {
                if (tattr[i].GetType() == typeof(Data.Mapping.Source))
                {
                    string view = (tattr[i] as Data.Mapping.Source).View; ;
                    string update = (tattr[i] as Data.Mapping.Source).Update;

                    ViewSource = view == string.Empty ? type.Name : view;
                    UpdateSource = update == string.Empty ? type.Name : update;
                }
            }

            System.Reflection.MemberInfo[] minfoArray = type.GetMembers(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
            foreach (MemberInfo info in minfoArray)
            {
                //Get DataFields
                object[] mattr = info.GetCustomAttributes(typeof(Data.Mapping.DataField), true);

                if (mattr.Length > 0)
                {
                    string columnName = (mattr[0] as Mapping.DataField).FieldName;
                    string memberName = (mattr[0] as Mapping.DataField).MemberName == null ? info.Name : (mattr[0] as Mapping.DataField).MemberName;

                    bool isPrimaryKey = (mattr[0] as Mapping.DataField).PrimaryKey;
                    bool isIdentity = (mattr[0] as Mapping.DataField).Identity;
                    int size = (mattr[0] as Mapping.DataField).Size; 

                    MemberInfo memberInfo = GetMemberInfo(type, memberName);

                    bool viewOnly = (mattr[0] as Mapping.DataField).ViewOnly;

                    if (memberInfo.MemberType == MemberTypes.Property)
                        Add(new TypeColumn(columnName, info.Name, (memberInfo as PropertyInfo), viewOnly, isPrimaryKey, isIdentity, size));
                    else if (memberInfo.MemberType == MemberTypes.Field)
                        Add(new TypeColumn(columnName, info.Name, (memberInfo as FieldInfo), viewOnly, isPrimaryKey, isIdentity, size));

                }                
            }

            //Get Relations
            foreach (MemberInfo info in minfoArray)
            {
                object[] rattr = info.GetCustomAttributes(typeof(Data.Mapping.Association), true);

                if (rattr.Length > 0)
                {
                    Mapping.Association association = rattr[0] as Mapping.Association;

                    MemberInfo memberInfo = GetMemberInfo(type, association.SourceMember);
                    TypeRelation relation = null;

                    TypeColumn sourceKeyColumn = null;
                    var columns = from col in this where col.Name == association.SourceKey select col;
                    if (columns.Count() > 0)
                        sourceKeyColumn = columns.ElementAt(0);
                    

                    if (memberInfo.MemberType == MemberTypes.Property)
                    {
                        relation = new TypeRelation(association.Relationship,
                            (memberInfo as PropertyInfo).PropertyType,
                            association.SourceMember,
                            association.SourceKey,
                            association.ReferenceKey,
                            memberInfo,
                            sourceKeyColumn);
                    }
                    else if (memberInfo.MemberType == MemberTypes.Field)
                    {
                        relation = new TypeRelation(association.Relationship,
                            (memberInfo as FieldInfo).FieldType,
                            association.SourceMember,
                            association.SourceKey,
                            association.ReferenceKey,
                            memberInfo,
                            sourceKeyColumn);
                    }
                    Associations.Add(relation);
                }
            }

            MappedObjectType = type;
        }

        private MemberInfo GetMemberInfo(Type type, string memberName)
        {
            MemberInfo[] typeInfos = type.GetMember(memberName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
            if (typeInfos.Count() > 0)
                return typeInfos[0];
            else if (type.BaseType != null)
                return GetMemberInfo(type, memberName);
            return null;
        }

        public bool HasIdentity
        {
            get
            {                
                if (m_isHasIdentitySet)
                    return m_hasIdentity;
                var res = from column in this where column.IsIdentity select column;
                if (res.Count() > 0)
                {
                    m_hasIdentity = true;
                    m_isHasIdentitySet = true;
                }
                return m_hasIdentity;
            }
        }

        public TypeColumn IdentityColumn
        {
            get
            {
                if (!HasIdentity)
                    return null;

                if (m_identityColumn == null)
                {
                    var res = from column in this where column.IsIdentity select column;
                    if (res.Count() > 0)
                        m_identityColumn = res.ElementAt(0);                        
                }

                return m_identityColumn;
            }
        }

        public bool HasKey
        {
            get
            {
                if (m_isHasKeySet)
                    return m_hasKey;
                var res = from column in this where column.IsIdentity == true select column;
                if (res.Count() > 0)
                {
                    m_hasKey = true;
                    m_isHasKeySet = true;
                }
                return m_hasKey;
            }
        }

        public string GetDbColumn(string holderName)
        {
            var colNames = from col in this where col.ColumnHolder == holderName select col.Name;
            if (colNames.Count() > 0)
                return colNames.ElementAt(0);
            return holderName;
        }

        private static Dictionary<Type, TypeTable> m_typeSchemaCache;

        private static Dictionary<Type, TypeTable> TypeSchemaCache
        {
            get
            {
                if (m_typeSchemaCache == null)
                    m_typeSchemaCache = new Dictionary<Type, TypeTable>();
                return m_typeSchemaCache;
            }
        }

        internal static TypeTable NewTypeTable(Type type)
        {
            return new TypeTable(type);
        }

        internal static TypeTable GetTypeTableSchema(Type type)
        {
            if (!TypeSchemaCache.ContainsKey(type))
                TypeSchemaCache.Add(type, NewTypeTable(type));
            return TypeSchemaCache[type];
        }


        #region Serialization
        void IXmlSerializable.WriteXml(XmlWriter writer)
        {
            writer.WriteStartAttribute("Type");
            writer.WriteValue(TypeName);
            writer.WriteEndAttribute();

            writer.WriteStartAttribute("Assembly");
            writer.WriteValue(m_assemblyName);
            writer.WriteEndAttribute();

            writer.WriteStartAttribute("View");
            writer.WriteValue(ViewSource);            
            writer.WriteEndAttribute();
            writer.WriteStartAttribute("Update");
            writer.WriteValue(UpdateSource);
            writer.WriteEndAttribute();

            writer.WriteStartElement("Fields");
            foreach (TypeColumn column in this)
            {
                (column as IXmlSerializable).WriteXml(writer);                
            }
            writer.WriteEndElement();

            writer.WriteStartElement("Associations");
            foreach (TypeRelation relation in Associations)
            {
                (relation as IXmlSerializable).WriteXml(writer);
            }
            writer.WriteEndElement();
        }

        void IXmlSerializable.ReadXml(XmlReader reader)
        {
            Type type = null;
            if (reader.NodeType == XmlNodeType.Element)
            { 
                //Get table attributes
                TypeName = reader.GetAttribute("Type");
                ViewSource = reader.GetAttribute("View");
                UpdateSource = reader.GetAttribute("Update");
                string assembly = reader.GetAttribute("Assembly");

                //TODO find another way not to include assembly
                if ((ViewSource == null && UpdateSource == null) || assembly == null)
                { 
                    //TODO throw exception here
                }

                if (ViewSource == null && UpdateSource != null)
                    ViewSource = UpdateSource;

                if (UpdateSource == null && ViewSource != null)
                    UpdateSource = ViewSource;

                try
                {
                    type = Type.GetType(string.Format("{0}, {1}", TypeName, assembly), true);
                    
                }
                catch (Exception ex)
                {
                    //TODO Log Error
                }

                if (type != null)
                    MappedObjectType = type;

                while (reader.Read())
                {
                    if (reader.NodeType == XmlNodeType.Element && reader.Name == "Fields")
                    {
                        if (reader.IsEmptyElement)
                            continue;
                        while (reader.Read())
                        {
                            if (reader.NodeType == XmlNodeType.Element && reader.Name == "DataField")
                            {
                                TypeColumn column = new TypeColumn();
                                column.ReadXml(reader, type);
                                Add(column);
                            }
                            else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "Fields")
                                break;
                        }
                    }
                    else if (reader.NodeType == XmlNodeType.Element && reader.Name == "Associations")
                    {
                        if (reader.IsEmptyElement)
                            continue;
                        while (reader.Read())
                        {
                            if (reader.NodeType == XmlNodeType.Element && reader.Name == "Association")
                            {
                                TypeRelation relation = new TypeRelation();                                

                                relation.ReadXml(reader, type, this);
                                Associations.Add(relation);
                            }
                            else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "Associations")
                                break;
                        
                        }
                    }
                    else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "DataObject")
                        break;                   
                    
                } 
            }

                           
        }

        XmlSchema IXmlSerializable.GetSchema()
        {
            return (null);
        }
        
        #endregion
    }
}
